/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.mocking;

import org.jmock.core.AbstractDynamicMock;
import org.jmock.core.Formatting;
import org.jmock.core.Stub;
import org.jmock.core.stub.ThrowStub;

import picounit.PicoUnitException;
import picounit.Should;
import picounit.features.base.DefaultStubFactory;
import picounit.features.base.ProxyFactory;
import picounit.features.constraint_store.ConstraintStore;
import picounit.features.registry.ClassUtil;
import picounit.mocker.ArrayConsequenceCapturer;
import picounit.mocker.ArrayConsequenceMatcher;
import picounit.mocker.BooleanArrayConsequenceCapturer;
import picounit.mocker.BooleanArrayConsequenceMatcher;
import picounit.mocker.BooleanConsequenceCapturer;
import picounit.mocker.BooleanConsequenceMatcher;
import picounit.mocker.ByteArrayConsequenceCapturer;
import picounit.mocker.ByteArrayConsequenceMatcher;
import picounit.mocker.ByteConsequenceCapturer;
import picounit.mocker.ByteConsequenceMatcher;
import picounit.mocker.CharArrayConsequenceCapturer;
import picounit.mocker.CharArrayConsequenceMatcher;
import picounit.mocker.CharConsequenceCapturer;
import picounit.mocker.CharConsequenceMatcher;
import picounit.mocker.ConsequenceCapturer;
import picounit.mocker.ConsequenceMatcher;
import picounit.mocker.DoubleArrayConsequenceCapturer;
import picounit.mocker.DoubleArrayConsequenceMatcher;
import picounit.mocker.DoubleConsequenceCapturer;
import picounit.mocker.DoubleConsequenceMatcher;
import picounit.mocker.EventCapturer;
import picounit.mocker.FloatArrayConsequenceCapturer;
import picounit.mocker.FloatArrayConsequenceMatcher;
import picounit.mocker.FloatConsequenceCapturer;
import picounit.mocker.FloatConsequenceMatcher;
import picounit.mocker.IntArrayConsequenceCapturer;
import picounit.mocker.IntArrayConsequenceMatcher;
import picounit.mocker.IntConsequenceCapturer;
import picounit.mocker.IntConsequenceMatcher;
import picounit.mocker.ListConsequenceCapturer;
import picounit.mocker.ListConsequenceMatcher;
import picounit.mocker.LongArrayConsequenceCapturer;
import picounit.mocker.LongArrayConsequenceMatcher;
import picounit.mocker.LongConsequenceCapturer;
import picounit.mocker.LongConsequenceMatcher;
import picounit.mocker.MockControl;
import picounit.mocker.MockFactory;
import picounit.mocker.OccurencesMatcher;
import picounit.mocker.PostConsequenceMatcher;
import picounit.mocker.SetConsequenceCapturer;
import picounit.mocker.SetConsequenceMatcher;
import picounit.mocker.ShortArrayConsequenceCapturer;
import picounit.mocker.ShortArrayConsequenceMatcher;
import picounit.mocker.ShortConsequenceCapturer;
import picounit.mocker.ShortConsequenceMatcher;
import picounit.mocker.StringArrayConsequenceCapturer;
import picounit.mocker.StringArrayConsequenceMatcher;
import picounit.mocker.StringConsequenceCapturer;
import picounit.mocker.StringConsequenceMatcher;
import picounit.mocker.VoidAction;

import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class JMocker implements Should, Verifiable, EventCapturer {
	private final List<RecordingPlaybackMock> mocks = new LinkedList<RecordingPlaybackMock>();

	private final ConsequenceMatchers consequenceMatchers;
	
	private final OccurencesMatcher occurencesMatcher;

	private final RecordingPlaybackMockListener recordingPlaybackMockListener;
	private final ActiveRecordingPlaybackMock activeRecordingPlaybackMock;
	
	private final MockInvocationObserver mockInvocationObserver;
	private final ProxyFactory proxyFactory;
	private final MockControl mockControl;
	private final MockFactory mockFactory;
	private final TypeFactory typeFactory;

	private DefaultStubFactory stubFactory;

	private ReturnTypeModifier returnTypeModifier;

	private ConstraintStore constraintStore;

	public JMocker(OccurencesMatcher occurencesMatcher,
		ActiveRecordingPlaybackMock activeRecordingPlaybackMock,
		RecordingPlaybackMockListener recordingPlaybackMockListener,
		MockInvocationObserver mockInvocationObserver,
		ProxyFactory proxyFactory,
		ReturnTypeModifier returnTypeModifier,
		ConstraintStore constraintStore) {

		this.occurencesMatcher = occurencesMatcher;
		this.activeRecordingPlaybackMock = activeRecordingPlaybackMock;
		this.recordingPlaybackMockListener = recordingPlaybackMockListener;
		this.mockInvocationObserver = mockInvocationObserver;
		this.proxyFactory = proxyFactory;
		this.returnTypeModifier = returnTypeModifier;
		this.constraintStore = constraintStore;

		this.consequenceMatchers =
			new ConsequenceMatchers(activeRecordingPlaybackMock, occurencesMatcher, mockInvocationObserver);
		this.stubFactory = new DefaultStubFactory(proxyFactory);

		this.mockControl = new DefaultMockControl(this);
		this.mockFactory = new DefaultMockFactory(this);

		this.typeFactory = new TypeFactory(stubFactory, mockFactory(), new ClassUtil());
	}
	
	public MockControl mockControl() {
		return mockControl;
	}

	public MockFactory mockFactory() {
		return mockFactory;
	}

	public TypeFactory typeFactory() {
		return typeFactory;
	}
	
	public void doAboveWhen() {
		expectAboveWhenTheFollowingOccurs();
	}
	
	public void record() {
		for (Object mock : mocks) {
			recordingPlaybackMock(mock).record();
		}
	}
	
	public void record(Object toRecord) {
		for (Object mock : mocks) {
			RecordingPlaybackMock recordingPlaybackMock = (RecordingPlaybackMock) mock;

			if (recordingPlaybackMock.proxy() == toRecord) {
				recordingPlaybackMock.record();
			}
		}
	}

	public <T> T mock(Class<T> mockedType) {
		return mock(mockedType, AbstractDynamicMock.mockNameFromClass(mockedType));
	}

	public <T> T mock(Class<T> mockedType, String name) {
		RecordingPlaybackMock<T> recordingPlaybackMock = recordingPlaybackMock(mockedType, name);

		mocks.add(recordingPlaybackMock);

		return recordingPlaybackMock.proxy();
	}

	public <T> T stub(Class<T> stubbedType, Object implementation) {
		return stub(stubbedType, implementation, "stub" + Formatting.classShortName(stubbedType));
	}

	public <T> T stub(Class<T> stubbedType, Object implementation, String name) {
		RecordingPlaybackMock<T> recordingPlaybackMock = recordingPlaybackMock(stubbedType, name); 

		recordingPlaybackMock.setDefaultStub(new DelegateToImplementationStub(implementation));

		mocks.add(recordingPlaybackMock);

		recordingPlaybackMock.replay();

		return recordingPlaybackMock.proxy();
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// VoidMethodMatcher
	///////////////////////////////////////////////////////////////////////////////////////////////
	
	public OccurencesMatcher raise(Throwable throwable) {
		return will(new ThrowStub(throwable));
	}
	
	public OccurencesMatcher perform(VoidAction voidAction) {
		return will(new VoidActionStub(voidAction));
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////
	// call
	///////////////////////////////////////////////////////////////////////////////////////////////
	
	public BooleanConsequenceMatcher call(boolean ignore) {
		return combinedConsequenceMatcher();
	}

	public ByteConsequenceMatcher call(byte ignore) {
		return combinedConsequenceMatcher();
	}

	public CharConsequenceMatcher call(char ignore) {
		return combinedConsequenceMatcher();
	}

	public DoubleConsequenceMatcher call(double ignore) {
		return combinedConsequenceMatcher();
	}

	public FloatConsequenceMatcher call(float ignore) {
		return combinedConsequenceMatcher();
	}

	public IntConsequenceMatcher call(int ignore) {
		return combinedConsequenceMatcher();
	}

	public LongConsequenceMatcher call(long ignore) {
		return combinedConsequenceMatcher();
	}

	public ShortConsequenceMatcher call(short ignore) {
		return combinedConsequenceMatcher();
	}

	public StringConsequenceMatcher call(String ignore) {
		return combinedConsequenceMatcher();
	}

	@SuppressWarnings("unchecked")
	public <T> ConsequenceMatcher<T> call(T ignore) {
		return combinedConsequenceMatcher();
	}

	public BooleanArrayConsequenceMatcher call(boolean[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public ByteArrayConsequenceMatcher call(byte[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public CharArrayConsequenceMatcher call(char[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public DoubleArrayConsequenceMatcher call(double[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public FloatArrayConsequenceMatcher call(float[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public IntArrayConsequenceMatcher call(int[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public LongArrayConsequenceMatcher call(long[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public ShortArrayConsequenceMatcher call(short[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	public StringArrayConsequenceMatcher call(String[] ignore) {
		return combinedArrayConsequenceMatcher();
	}

	@SuppressWarnings("unchecked")
	public <T> ArrayConsequenceMatcher<T> call(T[] ignore) {
		return combinedArrayConsequenceMatcher();
	}
	
	@SuppressWarnings("unchecked")
	public <T> ListConsequenceMatcher<T> call(List<T> ignore) {
		return combinedListConsequenceMatcher();
	}
	
	@SuppressWarnings("unchecked")
	public <T> SetConsequenceMatcher<T> call(Set<T> ignore) {
		return combinedSetConsequenceMatcher();
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// notCall
	///////////////////////////////////////////////////////////////////////////////////////////////

	public PostConsequenceMatcher notCall(boolean ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(byte ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(char ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(double ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(float ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(int ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(long ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(short ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(String ignore) {
		return throwExceptionIfInvoked();
	}
	
	public PostConsequenceMatcher notCall(Object ignore) {
		return throwExceptionIfInvoked();
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////
	//
	///////////////////////////////////////////////////////////////////////////////////////////////
	
	public EventCapturer expectTo(String reason) {
		occurencesMatcher.because(reason);

		return this;
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////
	// EventCapturer Methods
	///////////////////////////////////////////////////////////////////////////////////////////////

	public void byCalling() {
	}

	public BooleanConsequenceCapturer byCalling(boolean ignore) {
		return combinedConsequenceMatcher();
	}
	
	public ByteConsequenceCapturer byCalling(byte ignore) {
		return combinedConsequenceMatcher();
	}
	
	public CharConsequenceCapturer byCalling(char ignore) {
		return combinedConsequenceMatcher();
	}
	
	public DoubleConsequenceCapturer byCalling(double ignore) {
		return combinedConsequenceMatcher();
	}
	
	public FloatConsequenceCapturer byCalling(float ignore) {
		return combinedConsequenceMatcher();
	}
	
	public IntConsequenceCapturer byCalling(int ignore) {
		return combinedConsequenceMatcher();
	}
	
	public LongConsequenceCapturer byCalling(long ignore) {
		return combinedConsequenceMatcher();
	}
	
	public ShortConsequenceCapturer byCalling(short ignore) {
		return combinedConsequenceMatcher();
	}
	
	public StringConsequenceCapturer byCalling(String ignore) {
		return combinedConsequenceMatcher();
	}

	@SuppressWarnings("unchecked")
	public <T> ConsequenceCapturer<T> byCalling(T ignore) {
		return combinedConsequenceMatcher();
	}
	
	@SuppressWarnings("unchecked")
	public <T> ListConsequenceCapturer<T> byCalling(List<T> ignore) {
		return combinedListConsequenceMatcher();
	}
	
	@SuppressWarnings("unchecked")
	public <T> SetConsequenceCapturer<T> byCalling(Set<T> ignore) {
		return combinedSetConsequenceMatcher();
	}

	public BooleanArrayConsequenceCapturer byCalling(boolean ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public ByteArrayConsequenceCapturer byCalling(byte ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public CharArrayConsequenceCapturer byCalling(char ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public DoubleArrayConsequenceCapturer byCalling(double ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public FloatArrayConsequenceCapturer byCalling(float ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public IntArrayConsequenceCapturer byCalling(int ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public LongArrayConsequenceCapturer byCalling(long ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public ShortArrayConsequenceCapturer byCalling(short ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public StringArrayConsequenceCapturer byCalling(String ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	@SuppressWarnings("unchecked")
	public <T> ArrayConsequenceCapturer<T> byCalling(T ... ignore) {
		return combinedArrayConsequenceMatcher();
	}

	public void verify() {
		for (Object mock : mocks) {
			recordingPlaybackMock(mock).verify();
		}

    	reset();
	}

	public void reset() {
		for (Object mock : mocks) {
			recordingPlaybackMock(mock).reset();
		}
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////
	// MockControl, Should
	///////////////////////////////////////////////////////////////////////////////////////////////

	public void expectAboveWhenTheFollowingOccurs() {
		for (Object mock : mocks) {
			recordingPlaybackMock(mock).replay();
		}
	}

	public void thenExpect() {
		reset();
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation
	///////////////////////////////////////////////////////////////////////////////////////////////

	private RecordingPlaybackMock recordingPlaybackMock(Object mock) {
		return (RecordingPlaybackMock) mock;
	}

	private OccurencesMatcher will(Stub stub) {
		activeRecordingPlaybackMock.setStub(stub);

		return occurencesMatcher;
	}

	private PostConsequenceMatcher throwExceptionIfInvoked() {
		return raise(new PicoUnitException("should not call")).occurs(0);
	}

	@SuppressWarnings("unchecked")
	private <T> RecordingPlaybackMock<T> recordingPlaybackMock(Class<T> mockedType, String name) {
		return new RecordingPlaybackMock<T>(
			mockedType, name, recordingPlaybackMockListener, mockInvocationObserver, proxyFactory,
			returnTypeModifier, constraintStore, typeFactory);
	}
	
	private CombinedConsequenceMatcher combinedConsequenceMatcher() {
		return consequenceMatchers.combinedConsequenceMatcher();
	}
	
	private CombinedArrayConsequenceMatcher combinedArrayConsequenceMatcher() {
		return consequenceMatchers.combinedArrayConsequenceMatcher();
	}
	
	private CombinedListConsequenceMatcher combinedListConsequenceMatcher() {
		return consequenceMatchers.combinedListConsequenceMatcher();
	}

	private CombinedSetConsequenceMatcher combinedSetConsequenceMatcher() {
		return consequenceMatchers.combinedSetConsequenceMatcher();
	}
}
